from copy import deepcopy
from threading import Thread
import time
import os
import os.path

from ma.commons.core.abstractmapper import AbstractMapper
from ma.commons.core.constants import *
from ma.commons.core.taskinprogress import TaskInProgress
from ma.commons.core.maptaskrunner import MapTaskRunner
from ma.commons.core.maptipinfo import MapTIPInfo
from ma.commons.core.processrunner import ProcessRunner
from ma.utils import location
import ma.const
import ma.log


class MapTIP(TaskInProgress,Thread):
    """This class derives from TaskInProgress and keeps functionality that is 
    relevant to a map task only
    """
    
    def __init__(self, nodetracker, maptip_info=None, struct_id=STRUCT_ID_DEFAULT):
        """arguments description:
        nodetracker is the ref of tracker this task will run on
        maptip_info is a MapTIPInfo kind of pre fabricated object sent to init
        """
        
        # get the logger
        self.__log = ma.log.get_logger("ma.mrimprov")
        
        TaskInProgress.__init__(self, nodetracker)
        Thread.__init__(self)
        
        #to overshadow inherited TaskInProgressInfo object with a MapTIPInfo object
        if maptip_info is None:
            # a mapTIPinfo with default setting with no affiliation to a job or task
            self.tip_info = MapTIPInfo(-1,-1)
        else:
            self.tip_info = deepcopy(maptip_info)
            
        self.struct_id = struct_id
        self.threads = []
        
        user_process_id = self.tip_info.returnTaskID()
        
        #the task runner object that will launch the user provided by TaskRunner
        user_map_module = ma.const.JobsXmlData.get_str_data(ma.const.xml_map_module_name, self.tip_info.job_id)
        user_map_class = ma.const.JobsXmlData.get_str_data(ma.const.xml_map_class_name, self.tip_info.job_id)
        user_runner_program = location.get_src_ma_path() + ma.const.JobsXmlData.get_filepath_str_data(ma.const.xml_mrplus_maptask_runner, self.tip_info.job_id)
        
        # get the filename which holds the reduce compute misc. output
        proc_temp_filename = ma.const.JobsXmlData.get_str_data(ma.const.xml_map_proc_temp_filename, self.tip_info.job_id, self.tip_info.task_id)
        ProcessRunner.store_pickled_data([self.tip_info], proc_temp_filename, self.tip_info.job_id)
        self.process_runner = ProcessRunner(user_runner_program, [user_map_module, user_map_class, self.tip_info.job_id, self.tip_info.task_id], user_process_id)
        
        #self.mapper = AbstractMapper(self.key_value_dict)
        
                    
    def localizeTask(self):
        """This function copies the input split for the map to work on from the
        HDFS to the local file system. Also it deletes any map output files from a 
        previous attempt of this map on this node
        """
        
        copied_inputs = []
                    
        self.input_dest_dir = ma.const.JobsXmlData.get_filepath_str_data(ma.const.xml_local_input_temp_dir, self.tip_info.job_id) + os.sep
        
        # localize input one by one
        for file in self.tip_info.input_data:
            # TODO: This input is coming from the HDFS interactor ... it could have multiple inputs
            dfs_input_filepath = ma.const.JobsXmlData.get_dfs_filepath_str_data(ma.const.xml_dfs_path_job_input, self.tip_info.job_id) + ma.const.dfs_dir_sep + file[0]
            
            self.__log.debug('Copying map input for %s: %s', self.tip_info.returnTaskID(), str(dfs_input_filepath))
            
            input_dest_filepath = os.path.join(self.input_dest_dir, os.path.basename(dfs_input_filepath))
            if os.path.isfile(input_dest_filepath):
                self.__log.info('Deleting old local file: %s', input_dest_filepath)
                os.remove(input_dest_filepath)
            
            # TODO: Offset dealing while copying
            ret = self.nodetracker.hdfs.copy_file_to_local(dfs_input_filepath, self.input_dest_dir, self.tip_info.job_id, auto_retry=True)
            
            copied_inputs.append(file[0])
            
            #if transfer wasn't successful
            if not ret:
                self.__log.error("Localize Task Failed for map-task %d", self.tip_info.task_id)
                return False
        
        self.__log.info('Copied all map inputs for %s: %s', self.tip_info.returnTaskID(), str(copied_inputs))
        
        return True
        
                        
    def launchTask(self):
        """this function will launch the ProcessRunner. It returns true on
        success
        """ 
        
        arglist = [self.tip_info.job_id, self.tip_info.task_id, MAP]
        self.pingtimer_id = self.nodetracker.timer.addTimer(self.task_ping_interval, self.nodetracker.healthPingFromTask, arglist)
        
        # note the starting time
        self.tip_info.start_time = time.time()
        
        # start the process ... and wait till it ends
        ret = self.process_runner.start_and_end_process()
        
        # note the end time
        self.tip_info.end_time = time.time()
        
        return ret
        
    
    def taskComplete(self):
        """this function will indicate that the user map func has ended; in response
        this func must broadcast to the other NTs 
        that a certain map is complete and its output is up for grabs! 
        """
        
        # do the cleanup, delete the input files
        for file in self.tip_info.input_data:
            input_filepath = os.path.join(self.input_dest_dir, file[0])
            os.remove(input_filepath)
            
        self.__log.info('Map task %s completed processing', self.tip_info.returnTaskID())
        
        self.nodetracker.taskComplete(self.tip_info)
        
        
    def run(self):
        """This function will run as this thread is started and marks the flow of 
        how this MapTIP will function
        """
        
        self.__log.info('Started MapTIP for %s', self.tip_info.returnTaskID())
        
        # if localize didn't happen correctly
        if not self.localizeTask():
            self.nodetracker.killTask(self.tip_info.job_id, MAP, self.tip_info.task_id)
            self.__log.error('Couldn\'t localize task for Map %s', self.tip_info.returnTaskID())
            raise IOError('The MapTIP couldn\'t shuffle data for Map %d' % self.tip_info.task_id)
        
        #launch and finish task
        if self.launchTask():   
            #marks task as complete
            self.taskComplete()
        else:
            # TODO: Called failed task
            self.__log.error('Map task %s failed while processing', self.tip_info.returnTaskID())
            
        self.nodetracker.timer.removeTimer(self.pingtimer_id)
        #running in a separate process and if the process dies before completion 
        #it shud just remove the ping timer so that the health pings stop and 
        #the node tracker registers that the task is dead!
        #subprocess Popen object has a poll method!
        
        #send input to user func in a loop line by line ...is that ok!?
    